package commons.data;

import commons.data.Model.TypeField;
import commons.data.category.Category;
import commons.data.category.Filter;
import gnu.trove.set.hash.THashSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;


public class Attributes {

	private Model model;
	public Model getModel() {return model;}

	// On garde les données sous forme de chaine, pour plus de simplicité de traitement
	// Les valeurs seront dans le même ordre que les champs décrit dans le modele
	private String[] tabValues;

	// Cas des éléments linéaires : on a également un Attributes fusion
	private Attributes attr_fusion;
	private Model model_fusion;
	public Model getModelFusion() {return model_fusion;}


	// Ensemble des catégories de l'Attributes
	// sera crée et alimentée par affecteCategories(), lancée par TraitementInputData, avant la fusion
	// en effet, les catégory peuvent dépendre de champs qui auront disparu après la fusion ...
//	private HashMap<String,Category> mapCategory;
	private THashSet<String> setCategory;
	
	// uniquement pour retrouver l'ordre des category telles que définies par l'utilisateur
	//  en effet, la map met les category dans le désordre
	private ArrayList<Category> vAllCategory_forOrder;
	public Collection<String> getCategoriesName() {return setCategory;}
	

	public Attributes(Model model) {
		this.model = model;
		setCategory = null;
		tabValues = new String[model.getNumberOfField()];

		for (int i = 0; i <model.getNumberOfField(); i++)
			tabValues[i] = "$$NOT_SET$$";

		// (eventuellement remplacée par des null ...)
		invalidateHashCode();
	}
	
	public boolean hasValueNOTSET() {
		for (int i = 0; i <tabValues.length; i++)
			if (tabValues[i].equals("$$NOT_SET$$"))
				return true;
		return false;
	}

	public Attributes(Model model, String[] tabAttributes) {
		this(model);
		for (int i = 0; i < tabAttributes.length ; i++)
			setValue(i, tabAttributes[i]);
	}

	public Attributes(Model model, THashSet<String> setCategory, ArrayList<Category> vAllCategory_forOrder) {
		this(model);
		this.setCategory = setCategory;
		this.vAllCategory_forOrder = vAllCategory_forOrder;
	}

	// Utilisé si ajout par l'algo d'attributs supplémentaires à ceux du fichier lu
	//  -> il faut compléter le Model, puis faire de la place dans chaque Attributes
	//     de tous les objets pour recueillir ces valeurs additionnelles
	public void addAttributes(int nbAttributes) {
		String[] newTabValues = new String[tabValues.length + nbAttributes];
		for (int i=0 ; i<tabValues.length ; i++)
			newTabValues[i] = tabValues[i];
		// Les nouvelles valeurs possibles sont initialisée à vide
		for (int i=tabValues.length ; i<newTabValues.length ; i++)
			newTabValues[i] = "";
		
		tabValues = newTabValues;
	}

	public void clear() {
		if (model == null) return; // deja cleared !
		model = null;

		if (model_fusion != null)
			model_fusion.clear();
		model_fusion = null;
		
		tabValues = null;

		if (setCategory != null) {
			setCategory.clear();
			setCategory = null;
		}

		if (vAllCategory_forOrder != null) {
			vAllCategory_forOrder.clear();
			vAllCategory_forOrder = null;
		}

		if (attr_fusion != null) {
			attr_fusion.clear();
			attr_fusion = null;
		}
	}

	
	public void setValue(String fieldName, String value) {
		setValue(model.getIndexOf(fieldName), value);
	}
	public void setValue(int index, String value) {
		assert(value!=null);
		invalidateHashCode(); // le hashCode stocké n'est plus valide
		tabValues[index] = value;
	}

	public TypeField getType (String fieldName) {
		return model.getTypeOf(fieldName);
	}

	public TypeField getType (int index) {
		return model.getTypeOf(index);
	}

	public String getValue (String fieldName) {
		return tabValues[model.getIndexOf(fieldName)];
	}
	public String getValue (int index) {
		return tabValues[index];
	}

	public boolean existField (String fieldName) {
		return model.existField(fieldName);
	}

	public Attributes createAttributeFusionne (Model model_fusion) {
		// test si on a déjà calculé
		// en pratique, model_fusion sera toujours le même (mais on fait le test au cas où
		if (this.model_fusion == model_fusion)
			return this.attr_fusion;
		
		this.model_fusion = model_fusion;
		this.attr_fusion = new Attributes(model_fusion, setCategory, vAllCategory_forOrder);

		// Parcourt des champs du modèle fusionné
		// il faudra mettre les valeurs dans le meme ordre !
		for (int i = 0; i < model_fusion.getNumberOfField(); i++) {
			String fieldName = model_fusion.getFieldName(i);
			this.attr_fusion.setValue(fieldName, this.getValue(fieldName));
		}
		return this.attr_fusion;
	}


	// variable pour optimiser les equals()
	private int hashCode;
	protected void computeHashCode() {
		// On se base sur l'algo des Vector
		hashCode = 1;
		for (int i=0 ; i<tabValues.length ; i++)
			hashCode = 31*hashCode + (tabValues[i].hashCode());
	}
	private void invalidateHashCode() {	hashCode = -1; }
	

	@Override
	public boolean equals(Object obj) {
		// ce test simple va fonctionner de nombreuses fois, car les Attributes sont partagés
		// entre les segment_in d'un même objets SIG d'entrée
		if (obj == this)
			return true;

		Attributes attrib2 = (Attributes) obj;

		//*** Test rapide de non égalité, à base d'hashCode précalculés ********
		// On s'assure que les 2 hashCode ont bien été calculés
		if (this.hashCode==-1) computeHashCode();
		if (attrib2.hashCode==-1) attrib2.computeHashCode();

		if (this.hashCode != attrib2.hashCode)
			return false;
		//**********************************************************************
		int nbField = this.model.getNumberOfField();
		for (int i = 0; i <nbField; i++) {
			if (! this.getValue(i).equals(attrib2.getValue(i)))
				return false;
		}
		// arrivés là, c'est que tout est égal
		return true;
	}


	// A priori, plus utilisé depuis la v17 , car utilisé lors de la fusion des segment en v<17
	public int compareTo(Attributes attrib2) {
		int comparaison;

		// Ce test initial d'égalité, (qui est optimisé), va faire gagner du temps, car les attributes seront souvent égaux
		if (this.equals(attrib2))
			return 0;

		// TODO_BOF OPTIM : remonter le calcul du nombre de field
		int nbField = this.model.getNumberOfField();
		for (int i = 0; i <nbField; i++) {
			comparaison = this.getValue(i).compareTo(attrib2.getValue(i));
			if (comparaison != 0)
				return comparaison;
		}
		return 0;
	}


	@Override
	public String toString() {
		return toString("");
	}

	public String toString(String prefixe) {
		String res = "";
		for (int i = 0; i < model.getNumberOfField(); i++) {
			String fieldName = model.getFieldName(i);
			String value = tabValues[i];
			res += prefixe + (fieldName + "               ").substring(0, 15) + " = " + value + "\n";
		}
		return res;
	}

	// renvoie le détail des 10 premières valeurs non vides d'attributs
	public String toString_only10Values() {
		String res = "";
		int count = 0;
		int i;
		for (i = 0; i < model.getNumberOfField(); i++) {

			String fieldName = model.getFieldName(i);
			String value = tabValues[i];

			if (value.length()>0) {

				res += (fieldName + "               ").substring(0, 15) + " = " + value + "\n";

				count++;
				if (count>=10)
					break;
			}
		}

		if (i < model.getNumberOfField())
			res += "...\n";
		
		return res;
	}

	public void affecteCategories(ArrayList<Category> vCategory) {
		vAllCategory_forOrder = vCategory;
		setCategory = new THashSet<String>();
		for(Category cat : vCategory) {
			if (correspondToCategory(cat))
				setCategory.add(cat.name);
		}
	}

	private boolean correspondToCategory(Category cat) {
		// Optimisation
		// on a peut etre déjà testé cette catégorie :
		// dans le parcours de vCategory dans affecteCategories(),
		// certaines catégories inférieure à cat, sur lesquelles cat est basée directement
		// ou indirectement, on déjà été testées
		if (setCategory.contains(cat.name))
			return true;

		// Il faut passer tous les filtres de la category
		// -> appel recursif sur la category de base
		if (cat.categoryBase != null && !correspondToCategory(cat.categoryBase))
			return false;

		// Maintenant, il faut que l'attribute corresponde au filtre principal de la catégory
		if (cat.filter == Filter.allPassFilter)
			return true;
		else {
			String fieldValue = getValue(cat.filter.fieldName);
			return cat.filter.matchField(fieldValue);
		}
	}
	
	/**
	 * Demande si l'attributes fait partie de la category cat
	 * Ne fait aucun calcul de pattern :
	 *  n'utilise que la map contenant les category affectées
	 * Donc affecteCategories() doit avoir été lancé auparavant !
	 * @param cat
	 * @return
	 */
	public boolean isOfCategory (Category cat) {
		if (cat==null)
			return false;
		return setCategory.contains(cat.name);
	}

	public String getListCategoriesToString() {
		if (vAllCategory_forOrder==null) return ""; // cas des path virtuels reliant les nodes fusionnés temporaire (cf. algo >= V22)
		
		String res = "";
		for (Category cat : vAllCategory_forOrder)
			if (isOfCategory(cat))
				res += "  " + cat.name + "\n";
		return res;
	}

}
